package org.hertsig.magiccardmarket;

import static com.google.common.base.Preconditions.checkArgument;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.ssl.SSLContextBuilder;

import com.google.common.escape.Escaper;
import com.google.common.io.CharStreams;
import com.google.common.net.UrlEscapers;

/**
 * <p><a href=https://www.mkmapi.eu/ws/documentation>API documentation</a></p>
 * 
 * <p>
 * Metaproduct: All Forests from all sets<br/>
 * Product: Card 246 from M14
 * (Card 247 is a forest too, but has a different product id)<br/>
 * Article: The 2 foil, near mint Forests cards (246) from M14 offered by Jorn86
 * (a non-foil card or one offered by another seller has a different article id)
 * </p>
 */
@Slf4j
@NoArgsConstructor(access=AccessLevel.PROTECTED)
public abstract class MagicCardMarketApi {
	protected static final String BASE_URL = "https://www.mkmapi.eu/ws/";
	private static final Escaper ESCAPER = UrlEscapers.urlPathSegmentEscaper();
	
	@Getter(lazy=true) private final MarketplaceApi marketplaceApi = new MarketplaceApi(this);
	@Getter(lazy=true) private final StockApi stockApi = new StockApi(this);
	@Getter(lazy=true) private final WantsListApi wantsListApi = new WantsListApi(this);
	@Getter(lazy=true) private final OrderApi orderApi = new OrderApi(this);
	
	@SneakyThrows({GeneralSecurityException.class, IOException.class})
	protected static SSLContext createSslContext() {
		KeyStore keyStore = KeyStore.getInstance("jks");
		keyStore.load(MagicCardMarketApi.class.getResourceAsStream("keystore"), "keystore".toCharArray());
		return new SSLContextBuilder().loadTrustMaterial(keyStore).build();
	}
	
	public static MagicCardMarketApi createWithUrlAuth(String username, String apiKey) {
		return new UrlParamAuthApi(username, apiKey);
	}
	
	final URI games() {
		return uri(base() + "games");
	}
	
	/**
	 * Gets a single metaproduct. Response includes the card name in several (but apparently not all) languages,
	 * and a list of product IDs for this metaproduct.
	 */
	final URI metaproduct(String id) {
		return uri(base() + "metaproduct/" + escape(id));
	}
	
	/**
	 * Finds the specified metaproduct, then returns as {@link #metaproduct(String)}
	 * 
	 * @param cardName The exact card name, case insensitive
	 * @param gameId Game id from {@link Game}
	 * @param languageId Language id from {@link Language}
	 */
	final URI findMetaproduct(String cardName, int gameId, int languageId) {
		return uri(base() + "metaproduct/" + escape(cardName) + "/" + gameId + "/" + languageId);
	}
	
	final URI product(String productId) {
		return uri(base() + "product/" + escape(productId));
	}
	
	final URI findProducts(String search, int gameId, int languageId, boolean exact, int startIndex) {
		return uri(base() + "products/" + escape(search) + "/" + gameId + "/" + languageId + "/" + exact + "/" + startIndex);
	}
	
	final URI articles(String productId, int startIndex) {
		return uri(base() + "articles/" + escape(productId) + "/" + startIndex);
	}
	
	final URI user(String userId) {
		return uri(base() + "user/" + escape(userId));
	}
	
	/**
	 * @param userId Actor from {@link Actor}
	 * @param status Status from {@link Status}
	 * @return
	 */
	final URI orders(int actor, int status, int start) {
		return uri(base() + "orders/" + actor + "/" + status + '/' + start);
	}
	
	/**
	 * GET this url to query details for the given order<br/>
	 * PUT to this url to change the status of an order
	 */
	final URI order(String orderId) {
		return uri(base() + "order/" + escape(orderId));
	}
	
	/**
	 * GET this url to query the current contents of the shopping car<br/>
	 * PUT to this url to add or remove items from the card. Multiple items can be added <strong>or</strong> removed
	 * in one request, but you can't add one <strong>and</strong> remove another.<br/>
	 * DELETE to this url to clear the contents of the shopping cart
	 */
	final URI shoppingcart() {
		return uri(base() + "shoppingcart");
	}
	
	/**
	 * GET this url to check out the current contents of the shopping cart
	 * Returns the shipments created by this action.
	 * Seems to return HTTP 500 (Internal server error) when the shopping cart is empty
	 */
	final URI checkOut() {
		return uri(base() + "shoppingcart/checkout");
	}
	
	/**
	 * GET this url to get a list of articles offered by the logged in user<br/>
	 * @param nextStartIndex 
	 */
	final URI stock(int nextStartIndex) {
		return uri(base() + "stock/" + nextStartIndex);
	}
	
	/**
	 * POST to this url to add an article to the logged in user's stock<br/>
	 * PUT to this url to update an article with the specified values<br/>
	 * DELETE to this url to delete the article with the specified id<br/>
	 */
	final URI stock() {
		return uri(base() + "stock");
	}
	
	/**
	 * POST to this url to increase the amount of a single article in the logged in user's stock
	 */
	final URI stockIncrease(String articleId, int amount) {
		checkArgument(amount > 0);
		return uri(base() + "stock/article/" + escape(articleId) + "/increase/" + amount);
	}
	
	/**
	 * POST to this url to increase the amount of a single article in the logged in user's stock
	 */
	final URI stockDecrease(String articleId, int amount) {
		checkArgument(amount > 0);
		return uri(base() + "stock/article/" + escape(articleId) + "/decrease/" + amount);
	}
	
	final URI stock(String articleId) {
		return uri(base() + "stock/" + escape(articleId));
	}
	
	/**
	 * GET this url to get all wantslists<br/>
	 * POST to this url to create a new wantslist for the specified game
	 */
	final URI wantslist() {
		return uri(base() + "wantslist");
	}
	
	/**
	 * GET this url to get the contents of the specified wantslist<br/>
	 * PUT to this url to add, edit or remove items for the specified wantslist<br/>
	 * DELETE to this url to delete the wantslist with the specified id
	 */
	final URI wantslist(String wantslistId) {
		return uri(base() + "wantslist/" + escape(wantslistId));
	}
	
	/**
	 * The base URL for all calls. Contains the MagicCardMarket URL and the user name and API key.
	 */
	protected abstract String base();
	
	/**
	 * Escapes a given path segment so it's allowed to be used in an URL
	 */
	protected String escape(String pathSegment) {
		return ESCAPER.escape(pathSegment);
	}
	
	/**
	 * Transforms the string built by this class into an {@link URI} object
	 */
	@SneakyThrows(URISyntaxException.class)
	protected URI uri(String url) {
		return new URI(url);
	}
	
	@SneakyThrows(IOException.class)
	InputStream executeAndGet(HttpUriRequest request) {
		return execute(request).getEntity().getContent();
	}
	
	/**
	 * If this method throws an {@link SSLException}, the information in src/main/resources/org/hertsig/magiccardmarket/http/keystore may need to be updated
	 */
	@SneakyThrows(IOException.class)
	HttpResponse execute(HttpUriRequest request) {
		prepareRequest(request);
		HttpResponse response = getClient().execute(request);
		
		int code = response.getStatusLine().getStatusCode();
		log.info("Request to {} returned {}: {}", request.getURI(), code, response.getStatusLine().getReasonPhrase());
		if (code < 200 || code > 206) {
			try (Reader in = new InputStreamReader(response.getEntity().getContent())) {
				CharStreams.copy(in, System.out);
			}
		}
		return response;
	}
	
	protected void prepareRequest(HttpUriRequest request) {
		// override if needed
	}
	
	protected abstract HttpClient getClient();
	
	@NoArgsConstructor(access=AccessLevel.PRIVATE)
	public static final class Game {
		public static final int MTG = 1;
		public static final int WOW = 2;
		public static final int YUGIOH = 3;
		public static final int SPOILS = 4;
		public static final int TELPERINQUAR = 5;
	}
	
	@NoArgsConstructor(access=AccessLevel.PRIVATE)
	public static final class Language {
		public static final int ENGLISH = 1;
		public static final int FRENCH = 2;
		public static final int GERMAN = 3;
		public static final int SPANISH = 4;
		public static final int ITALIAN = 5;
		public static final int PORTUGESE = 8;
	}
	
	@NoArgsConstructor(access=AccessLevel.PRIVATE)
	public static final class Actor {
		public static final int SELLER = 1;
		public static final int BUYER = 2;
	}
	
	@NoArgsConstructor(access=AccessLevel.PRIVATE)
	public static final class Status {
		public static final int BOUGHT = 1;
		public static final int PAID = 2;
		public static final int SENT = 4;
		public static final int RECEIVED = 8;
		public static final int LOST = 32;
		public static final int CANCELLED = 128;
	}
	
	@NoArgsConstructor(access=AccessLevel.PRIVATE)
	public static final class Risk {
		public static final int NO = 0;
		public static final int LOW = 1;
		public static final int HIGH = 2;
	}
	
	@NoArgsConstructor(access=AccessLevel.PRIVATE)
	public static final class Reputation {
		public static final int NEW_SELLER = 0;
		public static final int OUTSTANDING = 1;
		public static final int VERY_GOOD = 2;
		public static final int GOOD = 3;
		public static final int AVERAGE = 4;
		public static final int BAD = 5;
	}
}
